In the following, we look at symmetric encryption algorithms. In symmetric crypto, we use the same key for encryption and decryption. Therefore, the two parties need to establish a secret key between them. Symmetric encryption can be up to 1000 times faster than asymmetric encryption. Given the support of some crypto algorithm in the CPU and at hardware level, even faster.
AES is based on Rijndael encryption algorithm, designed by Joan Daemen and Vincent Rijmen. It was one of the algorithms submitted to U.S. National Institute of Standards and Technology (NIST) to replace DES and 3DES. It was published in 1998 and accepted and standardized in 2001.
Number of rounds a function of key size
Today most implementations use the CPU support (Intel AES-NI)
To encrypt messages of arbitrary size with block ciphers, we use the following algorithms, called the modes of operation. They define how to encrypt each block of the plaintext to produce the corresponding cipher text block. Some of these are completely insecure (ECB) and should not be used.
In [ ]:
import os
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
key = os.urandom(16) # in bytes, 128 bits
iv = os.urandom(16)
In [ ]:
# ECB Mode, we only need a key
### *** DO NOT USE ECB. IT IS INSECURE *** ###
cipher = Cipher(algorithms.AES(key), modes.ECB(), backend=default_backend())
encryptor = cipher.encryptor()
# note that we don't need padding here, since len("PyCon 2017 Cypto") = 16
cipher_text = encryptor.update(b"PyCon 2017 Cypto") + encryptor.finalize()
In [ ]:
cipher_text
In [ ]:
print (len(cipher_text))
In [ ]:
decryptor = cipher.decryptor()
decryptor.update(cipher_text) + decryptor.finalize()
In [ ]:
# CBC Mode, we also need an IV
cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend())
encryptor = cipher.encryptor()
# note that we don't need padding here, since len("PyCon 2017 Cypto") = 16
cipher_text = encryptor.update(b"PyCon 2017 Cypto") + encryptor.finalize()
In [ ]:
cipher_text
In [ ]:
decryptor = cipher.decryptor()
decryptor.update(cipher_text) + decryptor.finalize()
In [ ]:
# CTR Mode, we don't need padding in CTR mode. In transforms a block cipher into a stream cipher
# we only need to introduce the nonce
cipher = Cipher(algorithms.AES(key), modes.CTR(os.urandom(16)), backend=default_backend())
encryptor = cipher.encryptor()
# len(b"PyCon 2017 Cypto!!") = 18, however no padding is needed.
cipher_text = encryptor.update(b"PyCon 2017 Cypto!!") + encryptor.finalize()
In [ ]:
plain_text = b"PyCon is great!!" * 128
In [ ]:
def print_text(text, b64=False):
for i in range(0, 128, 16):
if b64:
pt = base64.b64encode(text[i:i+16])
else:
pt = text[i:i+16]
print (pt)
In [ ]:
## IMPLEMENTATION
In [ ]:
print_text(ecb_ct)
In [ ]:
print_text(cbc_ct)
In [ ]:
print_text(ctr_ct)
Encrypt the file "include/tux.png" using the ECB, and CBC or CTR mode and compare the results.
To install Pillow, simply use pip or conda:
pip install Pillow
conda install pillow
The origianl image from Wikipedia.
ECB Encryption of the image with two different keys. The results (colors) are different, because we are using two different keys. However, the patterns inside the data (image) is not hidden.
As compared to when we are using the CBC (or CTR) mode. Because we introduce the randomness at the beginning (IV), and we carry this randomness (noise) throughout the encryption the patterns are diminished.
Since the IV is sent in clear we can change the IV value and change the corresponding plaintext, when using CBC mode.
Encryption
Decryption
Therefore to change the plaintext value we just need to xor the old plaintext(p), and the new value (t), with the IV:
$IV = IV \oplus p \oplus t$
Meaning if the first 4 bytes of the plaint text are: "1234" and we want to change it to "6789" all we have to do is
$IV[0:4] = IV[0:4] \oplus 1234 \oplus 6789$
In [ ]:
def xor(s1, s2):
return bytes([a ^ b for a,b in zip(s1,s2)])
In [ ]:
iv = os.urandom(16)
cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend())
encryptor = cipher.encryptor()
cipher_text = encryptor.update(b"PyCon2017 Crypto") + encryptor.finalize()
In [ ]:
cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend())
decryptor = cipher.decryptor()
decryptor.update(cipher_text) + decryptor.finalize()
In [ ]:
## IMPLEMENTATION
AEAD provides confidentiality, integrity, and authenticity at once. Such schemes help to mitigate against the bit flipping attacks that we just did. The Galois/Counter Mode (GCM) mode of operation is the recommended schemes to be used. Fortunately, the cryptography library already has it implemented.
In [ ]:
# GCM Mode, we also need an IV
cipher = Cipher(algorithms.AES(key), modes.GCM(iv), backend=default_backend())
encryptor = cipher.encryptor()
# note that we don't need padding here, since len("PyCon2017 Crypto") = 16
encryptor.authenticate_additional_data(b"SOME ADDITIONAL DATA")
cipher_text = encryptor.update(b"PyCon2017 Crypto") + encryptor.finalize()
tag = encryptor.tag
In [ ]:
decryptor = Cipher(algorithms.AES(key), modes.GCM(iv,tag), backend=default_backend()).decryptor()
decryptor.authenticate_additional_data(b"SOME ADDITIONAL DATA")
decryptor.update(cipher_text) + decryptor.finalize()
In [ ]:
import os
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
key = os.urandom(16) # in bytes, 128 bits
#CTR
cipher = Cipher(algorithms.AES(key), modes.CTR(os.urandom(16)), backend=default_backend())
encryptor = cipher.encryptor()
# len("PyCon 2017 Cryptography") = 23, but we don't need padding in CTR
ctr_ct = encryptor.update(b"PyCon 2017 Cryptography") + encryptor.finalize()
In [ ]:
#CBC
cipher = Cipher(algorithms.AES(key), modes.CBC(os.urandom(16)), backend=default_backend())
encryptor = cipher.encryptor()
# len("PyCon 2017 Cryptography") = 23, throws an exception
cbc_ct = encryptor.update(b"PyCon 2017 Cryptography") + encryptor.finalize()
PKCS7 padding is described in RFC 5652. The number of missing bytes (n) to the whole block size is repeated n times.
In [ ]:
## IMPLEMENTATION
def pkcs7_pad(text):
pass
In [ ]:
# Test our implementation of PKCS7 padding
from cryptography.hazmat.primitives import padding
for i in range(16):
msg = b'A'*i
padder = padding.PKCS7(128).padder()
padded_data = padder.update(msg)
padded_data += padder.finalize()
assert padded_data == pkcs7_pad(msg)